package org.force4spring;

import java.rmi.RemoteException;
import java.util.Map;
import java.util.Set;

import javax.xml.rpc.ServiceException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.force4spring.support.ForceSession;
import org.force4spring.support.SessionFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

import com.sforce.soap.partner.SforceServiceLocator;
import com.sforce.soap.partner.fault.ApiFault;
import com.sforce.soap.partner.fault.LoginFault;
import com.sforce.soap.partner.fault.UnexpectedErrorFault;

/**
 * Helper class featuring methods for Apex Session handling, allowing for reuse
 * of Apex Session instances within transactions. Also provides support for
 * exception translation.
 * 
 * <p>
 * Supports synchronization with Spring-managed JTA transactions (see
 * {@link org.springframework.transaction.jta.JtaTransactionManager}),
 * transparently providing transaction-scoped Apex Sessions.
 * 
 * <p>
 * Used internally by {@link ForceTemplate}, {@link ForceInterceptor} and
 * {@link ForceTransactionManager}. Can also be used directly in application
 * code.
 * 
 * @author Juergen Hoeller
 * @author Max Rudman
 * @since 09.01.2007
 * @see #getSession
 * @see #releaseSession
 * @see ForceTransactionManager
 * @see org.springframework.transaction.jta.JtaTransactionManager
 * @see org.springframework.transaction.support.TransactionSynchronizationManager
 */
public class SessionFactoryUtils {
	/**
	 * Order value for TransactionSynchronization objects that clean up
	 * Hibernate Sessions. Returns
	 * <code>DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100</code> to
	 * execute Session cleanup before JDBC Connection cleanup, if any.
	 * 
	 * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER
	 */
	public static final int SESSION_SYNCHRONIZATION_ORDER = DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100;

	private static final Log log = LogFactory.getLog(SessionFactoryUtils.class);
	
	private static final ThreadLocal deferredCloseHolder = new ThreadLocal();
	
	/**
	 * Get a new Force.com Session from the given SessionFactory.
	 * Will return a new ForceSession even if there already is a pre-bound
	 * Session for the given SessionFactory.
	 * <p>Within a transaction, this method will create a new Session
	 * that shares the transaction's JDBC Connection. More specifically,
	 * it will use the same JDBC Connection as the pre-bound Hibernate Session.
	 * @param sessionFactory Hibernate SessionFactory to create the session with
	 * @param entityInterceptor Hibernate entity interceptor, or <code>null</code> if none
	 * @return the new Session
	 */
	public static ForceSession getNewSession(SessionFactory sessionFactory) {
		Assert.notNull(sessionFactory, "No SessionFactory specified");
		try {
			SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
			if (sessionHolder != null && !sessionHolder.isEmpty()) {
				return sessionFactory.openSession(sessionHolder.getSession().getCredentials());
			}
			else {
				return sessionFactory.openSession();
			}
		}
		catch (RemoteException re) {
			throw new DataAccessResourceFailureException("Could not open Apex Session", re);
		}
		catch (ServiceException ex) {
			throw new DataAccessResourceFailureException("Could not open Apex Session", ex);
		}
	}

	public static ForceSession getSession(SessionFactory sessionFactory) {
		return getSession(sessionFactory, true);
	}

	public static ForceSession getSession(SessionFactory sessionFactory, boolean allowCreate) {
		Assert.notNull(sessionFactory, "No sessionFactory specified");

		SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
				.getResource(sessionFactory);
		
		if (sessionHolder != null && !sessionHolder.isEmpty()) {
			// pre-bound Hibernate Session
			ForceSession session = null;
			if (TransactionSynchronizationManager.isSynchronizationActive()
					&& sessionHolder.doesNotHoldNonDefaultSession()) {
				// Spring transaction management is active ->
				// register pre-bound Session with it for transactional
				// flushing.
				session = sessionHolder.getValidatedSession();
				if (!sessionHolder.isSynchronizedWithTransaction()) {
					log
							.debug("Registering Spring transaction synchronization for existing Force Session");
					TransactionSynchronizationManager
							.registerSynchronization(new SpringSessionSynchronization(
									sessionHolder, sessionFactory, false));
					sessionHolder.setSynchronizedWithTransaction(true);
				}
			}
			else {
				// No Spring transaction management active -> get the Session from SessionHolder
				session = sessionHolder.getValidatedSession();
			}

			if (session != null) {
				return session;
			}
		}

		try {
			log.debug("Starting Force.com Session");

			ForceSession session = sessionFactory.openSession();
			
			// Use same Session for further Apex actions within the transaction.
			// Thread object will get removed by synchronization at transaction
			// completion.
			if (TransactionSynchronizationManager.isSynchronizationActive()) {
				// We're within a Spring-managed transaction, possibly from
				// JtaTransactionManager.
				log
						.debug("Registering Spring transaction synchronization for new Force.com Session");
				SessionHolder holderToUse = sessionHolder;
				if (holderToUse == null) {
					holderToUse = new SessionHolder(session);
				} else {
					holderToUse.addSession(session);
				}

				TransactionSynchronizationManager
						.registerSynchronization(new SpringSessionSynchronization(
								holderToUse, sessionFactory, true));
				holderToUse.setSynchronizedWithTransaction(true);
				if (holderToUse != sessionHolder) {
					TransactionSynchronizationManager.bindResource(
							sessionFactory, holderToUse);
				}
			}

			// Check whether we are allowed to return the Session.
			if (!allowCreate
					&& !isSessionTransactional(session, sessionFactory)) {
				closeSession(session);
				throw new IllegalStateException(
						"No Force.com Session bound to thread, "
								+ "and configuration does not allow creation of non-transactional one here");
			}

			return session;
		} catch (ApiFault fault) {
			throw convertApiFault(fault);
		} catch (RemoteException re) {
			throw new DataAccessResourceFailureException(
					"Could not open Force.com Session", re);
		} catch (ServiceException re) {
			throw new DataAccessResourceFailureException(
					"Could not open Force.com Session", re);
		}
	}
	

	/**
	 * Return whether the given Apex Session is transactional, that is, bound to
	 * the current thread by Spring's transaction facilities.
	 * 
	 * @param session
	 *            the Apex Session to check
	 * @param sessionFactory
	 *            Apex SessionFactory that the ApexSession was created with (may
	 *            be <code>null</code>)
	 * @return whether the Session is transactional
	 */
	public static boolean isSessionTransactional(ForceSession session,
			SessionFactory sessionFactory) {
		if (sessionFactory == null) {
			return false;
		}
		SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
				.getResource(sessionFactory);
		return (sessionHolder != null && sessionHolder.containsSession(session));
	}

	/**
	 * Perform actual closing of the Apex Session, catching and logging any
	 * cleanup exceptions thrown.
	 * 
	 * @param session
	 *            the Hibernate Session to close (may be <code>null</code>)
	 * @see net.sf.hibernate.Session#close()
	 */
	public static void closeSession(ForceSession session) {
		if (session != null) {
			log.debug("Closing Apex Session");
			try {
				session.close();
			} catch (Throwable ex) {
				log.debug("Unexpected exception on closing Force Session",
						ex);
			}
		}
	}

	/**
	 * Return whether the given Hibernate Session is transactional, that is,
	 * bound to the current thread by Spring's transaction facilities.
	 * 
	 * @param session
	 *            the Hibernate Session to check
	 * @param sessionFactory
	 *            Hibernate SessionFactory that the Session was created with
	 *            (may be <code>null</code>)
	 * @return whether the Session is transactional
	 */
	public static boolean isSessionTransactional(ForceSession session,
			SforceServiceLocator serviceLocator) {
		if (serviceLocator == null) {
			return false;
		}
		SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
				.getResource(serviceLocator);
		return (sessionHolder != null && sessionHolder.containsSession(session));
	}

	public static DataAccessException convertApiFault(ApiFault fault) {
		if (fault instanceof LoginFault) {
			return new LoginException((LoginFault)fault);
		}
		
		if (fault instanceof UnexpectedErrorFault) {
			return new UnexpectedErrorException((UnexpectedErrorFault)fault);
		}
		
		return new GenericException(fault);
	}
	
	/**
	 * Close the given Session or register it for deferred close.
	 * @param session the Force Session to close
	 * @param sessionFactory Force SessionFactory that the Session was created with
	 * (may be <code>null</code>)
	 * @see #initDeferredClose
	 * @see #processDeferredClose
	 */
	static void closeSessionOrRegisterDeferredClose(ForceSession session, SessionFactory sessionFactory) {
		Map holderMap = (Map) deferredCloseHolder.get();
		if (holderMap != null && sessionFactory != null && holderMap.containsKey(sessionFactory)) {
			log.debug("Registering Force Session for deferred close");
			Set sessions = (Set) holderMap.get(sessionFactory);
			sessions.add(session);
		}
		else {
			closeSession(session);
		}
	}
	
	/**
	 * Close the given Session, created via the given factory,
	 * if it is not managed externally (i.e. not bound to the thread).
	 * @param session the Force.com Session to close (may be <code>null</code>)
	 * @param sessionFactory Force.com SessionFactory that the Session was created with
	 * (may be <code>null</code>)
	 */
	public static void releaseSession(ForceSession session, SessionFactory sessionFactory) {
		if (session == null) {
			return;
		}
		// Only close non-transactional Sessions.
		if (!isSessionTransactional(session, sessionFactory)) {
			closeSessionOrRegisterDeferredClose(session, sessionFactory);
		}
	}
}
